using System;
using System.Collections.Generic;
using System.Configuration;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using System.Text.RegularExpressions;
using System.Web;
using System.Web.Security;
using System.Xml;
using System.Xml.Linq;
using System.Xml.Serialization;
using SubversionReports.Entities;
using Path=SubversionReports.Entities.Path;

namespace SubversionReports.Utilities
{
    public static class SvnWebDav
    {
        #region repositories

        private readonly static string MasterRepositoryCredentialListCacheKey = "MasterRepositoryCredentialList";

        public static string DefaultRepositoryRootUrl = ConfigurationManager.AppSettings["DefaultRepositoryRootUrl"];
        public static string DefaultRepositoryUsername = ConfigurationManager.AppSettings["DefaultRepositoryUsername"];
        public static string DefaultRepositoryPassword = ConfigurationManager.AppSettings["DefaultRepositoryPassword"];

        /// <summary>
        /// Gets or sets the current repository root URL.
        /// </summary>
        /// <value>The current repository root URL.</value>
        public static string CurrentRepositoryRootUrl
        {
            get
            {
                return (HttpContext.Current != null && HttpContext.Current.Session != null && HttpContext.Current.Session["RepositoryRootUrl"] != null && HttpContext.Current.Session["RepositoryRootUrl"].ToString() != string.Empty)
                    ? HttpContext.Current.Session["RepositoryRootUrl"].ToString()
                    : DefaultRepositoryRootUrl;
            }
            set { HttpContext.Current.Session["RepositoryRootUrl"] = value; }
        }

        /// <summary>
        /// Gets or sets the current repository username.
        /// </summary>
        /// <value>The current repository username.</value>
        public static string CurrentRepositoryUsername
        {
            get
            {
                return (HttpContext.Current != null && HttpContext.Current.Session != null && HttpContext.Current.Session["RepositoryUsername"] != null)
                    ? HttpContext.Current.Session["RepositoryUsername"].ToString()
                    : CurrentRepositoryRootUrl.Equals(DefaultRepositoryRootUrl) ? DefaultRepositoryUsername : string.Empty;
            }
            set { HttpContext.Current.Session["RepositoryUsername"] = value; }
        }

        /// <summary>
        /// Gets or sets the current repository password.
        /// </summary>
        /// <value>The current repository password.</value>
        public static string CurrentRepositoryPassword
        {
            get
            {
                return (HttpContext.Current != null && HttpContext.Current.Session != null && HttpContext.Current.Session["RepositoryPassword"] != null)
                    ? HttpContext.Current.Session["RepositoryPassword"].ToString()
                    : CurrentRepositoryRootUrl.Equals(DefaultRepositoryRootUrl) ? DefaultRepositoryPassword : string.Empty;
            }
            set { HttpContext.Current.Session["RepositoryPassword"] = value; }
        }

        /// <summary>
        /// Gets or sets a value indicating whether [authentication suceeded].
        /// </summary>
        /// <value>
        /// 	<c>true</c> if [authentication suceeded]; otherwise, <c>false</c>.
        /// </value>
        public static bool AuthenticationSuceeded
        {
            get
            {
                return (HttpContext.Current != null && HttpContext.Current.Session != null && HttpContext.Current.Session["AuthenticationSuceeded"] != null)
                    ? (bool)HttpContext.Current.Session["AuthenticationSuceeded"]
                    : false;
            }
            set { HttpContext.Current.Session["AuthenticationSuceeded"] = value; }
        }

        #region public methods

        /// <summary>
        /// Gets all repository credentials for which the specified username and password is valid.
        /// </summary>
        /// <param name="username">The username.</param>
        /// <param name="password">The password.</param>
        /// <returns></returns>
        public static List<RepositoryCredential> GetUserRepositoryCredentials(string username, string password)
        {
            RepositoryCredentials cachedRepositoryCredentials = CacheHelper<RepositoryCredentials>.Get(MasterRepositoryCredentialListCacheKey);
            return cachedRepositoryCredentials.GetByUser(username, password);
        }

        /// <summary>
        /// Gets the repository list.
        /// </summary>
        /// <returns></returns>
        public static List<string> GetRepositoryList()
        {
            List<string> repositoryList = new List<string>();
            RepositoryCredentials cachedRepositoryCredentials = CacheHelper<RepositoryCredentials>.Get(MasterRepositoryCredentialListCacheKey);
            if(cachedRepositoryCredentials != null)
                cachedRepositoryCredentials.ForEach(delegate (RepositoryCredential rc)
                                                        {
                                                            if (!repositoryList.Contains(rc.RootUrl))
                                                                repositoryList.Add(rc.RootUrl);
                                                        });
            return repositoryList;
        }

        /// <summary>
        /// Checks whether anonymous login has been tested for the specified repositoryUrl.
        /// </summary>
        /// <param name="repositoryUrl">The repository URL.</param>
        /// <returns></returns>
        public static bool AnonymousLoginTested(string repositoryUrl)
        {
            RepositoryCredentials cachedRepositoryCredentials = CacheHelper<RepositoryCredentials>.Get(MasterRepositoryCredentialListCacheKey);
            return (cachedRepositoryCredentials != null && cachedRepositoryCredentials.Find(delegate(RepositoryCredential rc) { return (rc.RootUrl.Equals(repositoryUrl) && string.IsNullOrEmpty(rc.Username)); }) != null);
        }

        /// <summary>
        /// Adds the repository credential to the master list.
        /// </summary>
        /// <param name="repositoryUrl">The repository URL.</param>
        /// <param name="username">The username.</param>
        /// <param name="password">The password.</param>
        /// <returns></returns>
        public static Guid AddRepositoryCredential(string repositoryUrl, string username, string password)
        {
            string cleanUrl = GetGoogleCodeRepositoryRootUrl(repositoryUrl);
            var repositoryCredentials = CacheHelper<RepositoryCredentials>.Get(MasterRepositoryCredentialListCacheKey) ?? new RepositoryCredentials();
            var repositoryCredential = repositoryCredentials.GetByUrlAndUser(cleanUrl, username, password);
            if (repositoryCredential == null)
            {
                string rootUrl = GetRepositoryRootUrl(cleanUrl, username, password);
                AuthenticationSuceeded = !string.IsNullOrEmpty(rootUrl);
                if (AuthenticationSuceeded)
                {
                    repositoryCredential = new RepositoryCredential(rootUrl, username, password);
                    repositoryCredentials.Add(repositoryCredential);
                    CacheHelper<RepositoryCredentials>.Add(MasterRepositoryCredentialListCacheKey, repositoryCredentials); 
                }
            }
            else
            {
                AuthenticationSuceeded = true;
            }
            if (AuthenticationSuceeded && repositoryCredential != null)
            {
                CurrentRepositoryRootUrl = repositoryCredential.RootUrl;
                CurrentRepositoryUsername = repositoryCredential.Username;
                CurrentRepositoryPassword = repositoryCredential.Password;
                return repositoryCredential.Id;
            }
            return new Guid();
        }

        /// <summary>
        /// Gets the last revision number.
        /// </summary>
        /// <param name="url">The URL.</param>
        /// <param name="username">The username.</param>
        /// <param name="password">The password.</param>
        /// <returns></returns>
        public static int GetLastRevisionNumber(string url, string username, string password)
        {
            var cleanUrl = GetGoogleCodeRepositoryRootUrl(url);
            var rev = GetRepositoryRevision(new StreamReader(GetResponseStream(new Uri(cleanUrl), GetCredentialCache(cleanUrl, username, password))));
            int revision;
            if (!string.IsNullOrEmpty(rev) && int.TryParse(rev, out revision))
                return revision;

            string title = GetRepositoryTitle(url, username, password);
            int reportedRevision;
            if (!title.ToLower().StartsWith("revision"))
                title = title.Substring(title.ToLower().IndexOf("revision "));
            return (int.TryParse(title.Substring(9, title.IndexOf(":") - 9), out reportedRevision))
                ? reportedRevision
                : 1;
        }

        private static string GetRepositoryRevision(TextReader reader)
        {
            string rev;
            try
            {
                var xdoc = XDocument.Load(reader).Document;
                rev = xdoc != null && xdoc.Root != null
                    ? xdoc.Root.Descendants("index").Attributes("rev").First().Value
                    : null;
            }
            catch
            {
                rev = null;
            }
            return rev;
        }

        /// <summary>
        /// Gets the repository root URL.
        /// </summary>
        /// <param name="repositoryChildUrl">The repository child URL.</param>
        /// <param name="username">The username.</param>
        /// <param name="password">The password.</param>
        /// <returns></returns>
        public static string GetRepositoryRootUrl(string repositoryChildUrl, string username, string password)
        {
            var cleanUrl = GetGoogleCodeRepositoryRootUrl(repositoryChildUrl);
            var title = GetRepositoryTitle(cleanUrl, username, password);

            var rootFolder = title != null
                ? title.Contains(":")
                    ? title.Substring(title.IndexOf(":") + 1).Trim().ToLower()
                    : title
                : repositoryChildUrl.Replace(cleanUrl, string.Empty);

            return (!rootFolder.Equals("/") && !string.IsNullOrEmpty(rootFolder) && !string.IsNullOrEmpty(title))
                ? cleanUrl.Substring(0, cleanUrl.IndexOf(title)) + title + '/'
                : cleanUrl;

        }

        /// <summary>
        /// Gets the repository log from revision 1 to the last revision, using an anonymous webdav request.
        /// </summary>
        /// <param name="repositoryUrl">The repository URL.</param>
        /// <returns></returns>
        public static Repository GetRepositoryLog(string repositoryUrl)
        {
            Repository repository = GetRepositoryLog(repositoryUrl, null, null, 1, GetLastRevisionNumber(repositoryUrl, null, null));
            UpdateRepositorySummary(repository);
            return repository;
        }

        /// <summary>
        /// Gets the repository log from revision 1 to the last revision, using an authenticated webdav request.
        /// </summary>
        /// <param name="repositoryUrl">The repository URL.</param>
        /// <param name="username">The username.</param>
        /// <param name="password">The password.</param>
        /// <returns></returns>
        public static Repository GetRepositoryLog(string repositoryUrl, string username, string password)
        {
            Repository repository = GetRepositoryLog(repositoryUrl, username, password, 1, GetLastRevisionNumber(repositoryUrl, username, password));
            UpdateRepositorySummary(repository);
            return repository;
        }

        /// <summary>
        /// Updates the repository summary.
        /// </summary>
        /// <param name="repository">The repository.</param>
        private static void UpdateRepositorySummary(Repository repository)
        {
            RepositorySummary summary = new RepositorySummary();
            summary.RootUrl = repository.Url;
            summary.RevisionCount = repository.HighestRevision;
            summary.Contributors = repository.AuthorCommits;
            summary.LastQueried = DateTime.Now;
            summary.LastRevised = repository[repository.Count - 1].Date;
            Google.UpdateRepositorySummary(summary);
        }

        /// <summary>
        /// Gets the repository log from startRevision to endRevision, using an anonymous webdav request.
        /// </summary>
        /// <param name="repositoryUrl">The repository URL.</param>
        /// <param name="startRevision">The start revision.</param>
        /// <param name="endRevision">The end revision.</param>
        /// <returns></returns>
        public static Repository GetRepositoryLog(string repositoryUrl, int startRevision, int endRevision)
        {
            return GetRepositoryLog(repositoryUrl, null, null, startRevision, endRevision);
        }

        /// <summary>
        /// Gets the repository log from startRevision to endRevision, using an authenticated webdav request.
        /// This method contains the actual web dav call as well as calls to the filesystem and memory caches.
        /// </summary>
        /// <param name="repositoryUrl">The repository URL.</param>
        /// <param name="username">The username.</param>
        /// <param name="password">The password.</param>
        /// <param name="startRevision">The start revision.</param>
        /// <param name="endRevision">The end revision.</param>
        /// <returns></returns>
        private static Repository GetRepositoryLog(string repositoryUrl, string username, string password, int startRevision, int endRevision)
        {
            if (AuthenticationSuceeded)//dont even try if we haven't already had a successful authentication.
            {
                Repository repository = CacheHelper<Repository>.Get(GetRepositoryHash(repositoryUrl));
                if (repository != null && repository.HighestRevision < endRevision)
                {
                    //The repository is in the memory cache but it's missing recent revisions
                    AppendRecentRevisionsAndCache(repository, username, password, endRevision);
                }

                else if (repository == null)
                {
                    repository = GetRepositoryFromFileCache(repositoryUrl);
                    if (repository != null)
                    {
                        if (string.IsNullOrEmpty(repository.Url)) repository.Url = repositoryUrl;
                        CacheHelper<Repository>.Add(GetRepositoryHash(repositoryUrl), repository);
                        //The requested revision range was available in the filesystem cache
                        if (startRevision >= repository.LowestRevision && endRevision <= repository.HighestRevision)
                            return repository.Filter("Id", startRevision, endRevision, FilterType.Include);

                        //The requested revision range was partially available in the filesystem cache
                        else if (startRevision <= repository.LowestRevision && endRevision > repository.HighestRevision)
                        {
                            AppendRecentRevisionsAndCache(repository, username, password, endRevision);
                            if (startRevision.Equals(repository.LowestRevision) && endRevision.Equals(repository.HighestRevision))
                                return repository;
                            else return repository.Filter("Id", startRevision, endRevision, FilterType.Include);
                        }
                    }
                    else
                    {
                        repository = GetRepositoryFromWebDav(repositoryUrl, username, password, startRevision, endRevision);
                        if (repository.AuthorIcons.Count > 1)
                        {
                            repository.Description = GetRepositoryDescription(repository);
                            PutRepositoryInFileCache(repositoryUrl, repository);
                            CacheHelper<Repository>.Add(GetRepositoryHash(repositoryUrl), repository);
                            return repository;
                        }
                    }
                }

                if (string.IsNullOrEmpty(repository.Url)) repository.Url = repositoryUrl;
                //GetMissingRevisions(repository);
                repository.Description = GetRepositoryDescription(repository);

                return repository;
            }
            return null;
        }

        /// <summary>
        /// Gets the file revision from web dav.
        /// </summary>
        /// <param name="repositoryUrl">The repository URL.</param>
        /// <param name="username">The username.</param>
        /// <param name="password">The password.</param>
        /// <param name="revision">The revision.</param>
        /// <param name="path">The path.</param>
        /// <returns>The contents of the file specified by path if the file is readable, otherwise null.</returns>
        public static string GetFileRevisionFromWebDav(string repositoryUrl, string username, string password, int revision, string path)
        {
            return GetWebPage(string.Format("{0}!svn/bc/{1}{2}", repositoryUrl, revision, path), username, password);
        }

        #endregion

        #region private methods

        /// <summary>
        /// Gets the web page.
        /// </summary>
        /// <param name="requestUri">The request URI.</param>
        /// <param name="username">The username.</param>
        /// <param name="password">The password.</param>
        /// <returns></returns>
        private static string GetWebPage(string requestUri, string username, string password)
        {
            Stream responseStream = GetResponseStream(new Uri(requestUri), GetCredentialCache(requestUri, username, password));
            StreamReader reader = new StreamReader(responseStream);
            return responseStream.CanRead ? reader.ReadToEnd() : null;
        }

        /// <summary>
        /// Appends the recent revisions and caches to file and memory.
        /// </summary>
        /// <param name="repository">The repository.</param>
        /// <param name="username">The username.</param>
        /// <param name="password">The password.</param>
        /// <param name="lastRevision">The last revision.</param>
        private static void AppendRecentRevisionsAndCache(Repository repository, string username, string password, int lastRevision)
        {
            int startRevision = repository.Count > 0 ? repository.HighestRevision + 1 : 1;
            Repository missingRevisionsFromWebDav = GetRepositoryFromWebDav(repository.Url, username, password, startRevision, lastRevision);
            missingRevisionsFromWebDav.ForEach(delegate(Revision revision) { repository.Add(revision); });

            repository.Description = GetRepositoryDescription(missingRevisionsFromWebDav);
            repository.NonPagedCount = repository.Count;
            PutRepositoryInFileCache(repository.Url, repository);
            CacheHelper<Repository>.Add(GetRepositoryHash(repository.Url), repository);
        }

        /// <summary>
        /// Gets the repository description.
        /// </summary>
        /// <param name="repository">The repository.</param>
        /// <returns></returns>
        private static string GetRepositoryDescription(Repository repository)
        {
            Revision lastRevision = repository.Filter("Id", repository.HighestRevision, FilterType.Include)[0];
            return string.Format("Last revision: {0} by {1} on {2:dddd, dd/MM/yyyy} at {3:hh:mm:ss tt}", lastRevision.Id, lastRevision.Author, lastRevision.Date, lastRevision.Date);
        }

        /// <summary>
        /// Gets the repository from file cache.
        /// </summary>
        /// <param name="repositoryUrl">The repository URL.</param>
        /// <returns></returns>
        private static Repository GetRepositoryFromFileCache(string repositoryUrl)
        {
            string logFilePath = GetLogFilePath(repositoryUrl);
            if (File.Exists(logFilePath))
            {
                XmlSerializer serializer = new XmlSerializer(typeof(Repository));
                TextReader textReader = new StreamReader(logFilePath);
                Repository cachedRepository = (Repository)serializer.Deserialize(textReader);
                textReader.Close();
                return cachedRepository;
            }
            return null;
        }

        /// <summary>
        /// Puts the repository in file cache.
        /// </summary>
        /// <param name="repositoryUrl">The repository URL.</param>
        /// <param name="repository">The repository.</param>
        private static void PutRepositoryInFileCache(string repositoryUrl, Repository repository)
        {
            string logFilePath = GetLogFilePath(repositoryUrl);
            if (File.Exists(logFilePath))
                File.Delete(logFilePath);
            XmlSerializer serializer = new XmlSerializer(typeof (Repository));
            TextWriter textWriter = new StreamWriter(logFilePath);
            serializer.Serialize(textWriter, repository);
            textWriter.Close();
        }

        /// <summary>
        /// Gets the repository from web dav.
        /// </summary>
        /// <param name="repositoryUrl">The repository URL.</param>
        /// <param name="username">The username.</param>
        /// <param name="password">The password.</param>
        /// <param name="startRevision">The start revision.</param>
        /// <param name="endRevision">The end revision.</param>
        /// <returns></returns>
        private static Repository GetRepositoryFromWebDav(string repositoryUrl, string username, string password, int startRevision, int endRevision)
        {
            int increment = 300;
            if ((endRevision - startRevision <= increment))
                return GetRepositoryIncrementFromWebDav(repositoryUrl, username, password, startRevision, endRevision);
            else
            {
                Repository masterRepository = new Repository();
                masterRepository.Url = repositoryUrl;
                for (int i = startRevision; i < endRevision; i += increment)
                {
                    GetRepositoryIncrementFromWebDav(repositoryUrl, username, password, i, Math.Min(endRevision, (i + (increment - 1)))).ForEach(delegate(Revision revision) { masterRepository.Add(revision); });
                    PutRepositoryInFileCache(masterRepository.Url, masterRepository);
                }
                masterRepository.Description = GetRepositoryDescription(masterRepository);
                return masterRepository;
            }
        }

        /// <summary>
        /// Gets the repository increment from web dav.
        /// </summary>
        /// <param name="repositoryUrl">The repository URL.</param>
        /// <param name="username">The username.</param>
        /// <param name="password">The password.</param>
        /// <param name="startRevision">The start revision.</param>
        /// <param name="endRevision">The end revision.</param>
        /// <returns></returns>
        private static Repository GetRepositoryIncrementFromWebDav(string repositoryUrl, string username, string password, int startRevision, int endRevision)
        {
            string requestUri = string.Format("{0}!svn/bc/{1}", repositoryUrl, endRevision);
            XmlDocument response = GetPropFindResponse(
                requestUri,
                string.Format("<S:log-report xmlns:S=\"svn:\"><S:start-revision>{0}</S:start-revision><S:end-revision>{1}</S:end-revision><S:discover-changed-paths/><S:path></S:path></S:log-report>", endRevision, startRevision),
                GetCredentialCache(requestUri, username, password));
            Repository repository = GetRepositoryFromPropFindResponse(response);
            repository.Url = repositoryUrl;
            return repository;
        }

        /// <summary>
        /// Gets the location revision from web dav.
        /// This is an expensive method as it iteratively queries every revision until finding a change
        /// </summary>
        /// <param name="repositoryUrl">The repository URL.</param>
        /// <param name="username">The username.</param>
        /// <param name="password">The password.</param>
        /// <param name="pegRevision">The peg revision.</param>
        /// <param name="pegPath">The peg path.</param>
        /// <param name="locationRevision">The location revision.</param>
        /// <param name="locationPath">The location path.</param>
        /// <param name="getFirstKnownRevision">if set to <c>true</c> [get first known revision].</param>
        private static void GetLocationRevisionFromWebDav(string repositoryUrl, string username, string password, int pegRevision, string pegPath, out int locationRevision, out string locationPath, bool getFirstKnownRevision)
        {
            //string requestUri = string.Format("{0}!svn/bc/{1}{2}", repositoryUrl, pegRevision, pegPath);
            
            if(getFirstKnownRevision)
            {
                if(!GetLowestChangeRevision(repositoryUrl, username, password, pegPath, pegRevision, 1, out locationRevision, out locationPath))
                    throw new Exception("Wow, a path with no revision history! Fucked up!");
                //while (string.IsNullOrEmpty(locationPath) && (locationRevision < pegRevision))
                //{
                //    try
                //    {
                //        XmlDocument response = GetPropFindResponse(
                //            requestUri,
                //            string.Format("<S:get-locations xmlns:S=\"svn:\"><S:path></S:path><S:peg-revision>{0}</S:peg-revision><S:location-revision>{1}</S:location-revision></S:get-locations>", pegRevision, locationRevision),
                //            GetCredentialCache(requestUri, username, password));
                //        locationPath = GetLocationRevisionFromPropFindResponse(response);
                //    }
                //    catch
                //    {
                //        locationPath = null; //the path didn't exist in the requested revision
                //    }
                //    finally
                //    {
                //        locationRevision++;
                //    }
                //}
            }
            else
            {
                if(!GetHighestChangeRevision(repositoryUrl, username, password, pegPath, pegRevision, 1, out locationRevision, out locationPath))
                    throw new Exception("Wow, a path with no revision history! Fucked up!");
                //locationPath = pegPath;
                //locationRevision = pegRevision - 1;
                //while (pegPath.Equals(locationPath) && (locationRevision > 0))
                //{
                //    XmlDocument response = GetPropFindResponse(
                //        requestUri,
                //        string.Format("<S:get-locations xmlns:S=\"svn:\"><S:path></S:path><S:peg-revision>{0}</S:peg-revision><S:location-revision>{1}</S:location-revision></S:get-locations>", pegRevision, locationRevision),
                //        GetCredentialCache(requestUri, username, password));
                //    locationPath = GetLocationRevisionFromPropFindResponse(response);
                //    locationRevision--;
                //}
            }
        }

        private static bool GetLowestChangeRevision(string repositoryUrl, string username, string password, string path, int top, int bottom, out int changeRevision, out string changePath)
        {
            if (top.Equals(bottom) || top.Equals(bottom + 1))
            {
                changePath = path;
                changeRevision = top;
                return true;
            }
            string requestUri = string.Format("{0}!svn/bc/{1}{2}", repositoryUrl, top, path);
            try
            {
                XmlDocument response = GetPropFindResponse(
                    requestUri,
                    string.Format("<S:get-locations xmlns:S=\"svn:\"><S:path></S:path><S:peg-revision>{0}</S:peg-revision><S:location-revision>{1}</S:location-revision></S:get-locations>", top, bottom),
                    GetCredentialCache(requestUri, username, password));
                changePath = GetLocationRevisionFromPropFindResponse(response);
                if (!string.IsNullOrEmpty(changePath))
                {
                    //the path did exist in the requested revision, go lower
                    GetLowestChangeRevision(repositoryUrl, username, password, changePath, bottom, 1, out changeRevision, out changePath);
                }
                else
                {
                    //the path didn't exist in the requested revision, go higher
                    GetLowestChangeRevision(repositoryUrl, username, password, path, top, Middle(top, bottom), out changeRevision, out changePath);
                }
            }
            catch
            {
                if(top.Equals(bottom + 1))
                {
                    changePath = path;
                    changeRevision = top;
                    return true;
                }
                else
                {
                    //the path didn't exist in the requested revision, go higher
                    GetLowestChangeRevision(repositoryUrl, username, password, path, top, Middle(top, bottom), out changeRevision, out changePath);
                }
            }
            return (!string.IsNullOrEmpty(changePath));
        }

        private static bool GetHighestChangeRevision(string repositoryUrl, string username, string password, string path, int top, int bottom, out int changeRevision, out string changePath)
        {
            if(top.Equals(bottom))
            {
                changePath = path;
                changeRevision = top;
                return true;
            }
            string requestUri = string.Format("{0}!svn/bc/{1}{2}", repositoryUrl, top, path);
            XmlDocument response = GetPropFindResponse(
                requestUri,
                string.Format("<S:get-locations xmlns:S=\"svn:\"><S:path></S:path><S:peg-revision>{0}</S:peg-revision><S:location-revision>{1}</S:location-revision></S:get-locations>", top, bottom),
                GetCredentialCache(requestUri, username, password));
            changePath = GetLocationRevisionFromPropFindResponse(response);

            if (string.IsNullOrEmpty(changePath) || (!string.IsNullOrEmpty(changePath) && !changePath.Equals(path)))
            {
                //the path was changed or did not exist in the requested revision, go higher
                GetHighestChangeRevision(repositoryUrl, username, password, path, top, Middle(top, bottom), out changeRevision, out changePath);
            }
            else
            {
                //go lower
                GetHighestChangeRevision(repositoryUrl, username, password, path, bottom, 1, out changeRevision, out changePath);
            }

            return (!string.IsNullOrEmpty(changePath));
        }

        /// <summary>
        /// Gets the middle.
        /// </summary>
        /// <param name="top">The top.</param>
        /// <param name="bottom">The bottom.</param>
        /// <returns></returns>
        private static int Middle(int top, int bottom)
        {
            return (top - ((top - Floor(bottom)) / 2));
        }

        /// <summary>
        /// Floors the specified n.
        /// </summary>
        /// <param name="n">The n.</param>
        /// <returns></returns>
        private static int Floor(int n)
        {
            return (n % 2 == 1) ? (n - 1) : n;
        }

        /// <summary>
        /// Gets the credential cache.
        /// </summary>
        /// <param name="requestUri">The request URI.</param>
        /// <param name="username">The username.</param>
        /// <param name="password">The password.</param>
        /// <returns></returns>
        private static CredentialCache GetCredentialCache(string requestUri, string username, string password)
        {
            if (!string.IsNullOrEmpty(requestUri) && !string.IsNullOrEmpty(username) && !string.IsNullOrEmpty(password))
            {
                CredentialCache credentialCache = new CredentialCache();
                if (!string.IsNullOrEmpty(username) && !string.IsNullOrEmpty(password))
                    credentialCache.Add(new Uri(requestUri), "Basic", new NetworkCredential(username, password));
                return credentialCache;
            }
            return null;
        }

        /// <summary>
        /// Gets the repository from prop find response.
        /// </summary>
        /// <param name="propfindResponse">The propfind response.</param>
        /// <returns></returns>
        private static Repository GetRepositoryFromPropFindResponse(XmlDocument propfindResponse)
        {
            Repository repository = new Repository();
            foreach (XmlNode xmlLogEntry in propfindResponse.GetElementsByTagName("S:log-item"))
            {
                Revision revision = new Revision();
                revision.Id = int.Parse(xmlLogEntry["D:version-name"].InnerText);
                revision.Author = (xmlLogEntry["D:creator-displayname"] != null) ? xmlLogEntry["D:creator-displayname"].InnerText : string.Empty;
                revision.Comment = (xmlLogEntry["D:comment"] != null) ? xmlLogEntry["D:comment"].InnerText : string.Empty;
                revision.Date = (xmlLogEntry["S:date"] != null) ? DateTime.Parse(xmlLogEntry["S:date"].InnerText) : new DateTime();
                revision.Paths = new List<Path>();

                foreach (XmlNode pathNode in xmlLogEntry.ChildNodes)
                {
                    Path path = new Path();
                    switch (pathNode.Name)
                    {
                        case "S:added-path":
                            path.RevisionType = PathRevisionType.Added;
                            break;
                        case "S:deleted-path":
                            path.RevisionType = PathRevisionType.Deleted;
                            break;
                        case "S:modified-path":
                            path.RevisionType = PathRevisionType.Modified;
                            break;
                    }
                    if (!path.RevisionType.Equals(PathRevisionType.Unknown))
                    {
                        path.RevisionPath = pathNode.InnerText;
                        if (pathNode.Attributes["copyfrom-path"] != null)
                        {
                            path.CopyFromPath = pathNode.Attributes["copyfrom-path"].Value;
                            path.CopyFromRevision = int.Parse(pathNode.Attributes["copyfrom-rev"].Value);
                        }
                        revision.Paths.Add(path);
                    }
                }
                if (!revision.Date.Equals(new DateTime()))
                    repository.Add(revision);
            }
            repository.NonPagedCount = repository.Count;
            return repository;
        }

        /// <summary>
        /// Gets the location revision from prop find response.
        /// </summary>
        /// <param name="propfindResponse">The propfind response.</param>
        /// <returns></returns>
        private static string GetLocationRevisionFromPropFindResponse(XmlDocument propfindResponse)
        {
            foreach (XmlNode xmlNode in propfindResponse.GetElementsByTagName("S:location"))
                return xmlNode.Attributes["path"].Value;
            return null;
        }

        /// <summary>
        /// Gets the prop find response.
        /// </summary>
        /// <param name="requestUri">The request URI.</param>
        /// <param name="requestBody">The request body.</param>
        /// <param name="credentials">The credentials.</param>
        /// <returns></returns>
        private static XmlDocument GetPropFindResponse(string requestUri, string requestBody, ICredentials credentials)
        {
            XmlDocument propfindResponse = new XmlDocument();
            try
            {
                Stream responseStream = GetResponseStream(requestUri, requestBody, credentials, "REPORT", "text/xml");
                propfindResponse.Load(responseStream);
                //responseStream.Close();
            }
            catch (Exception ex) { throw new Exception(ex.Message, ex); }
            return propfindResponse;
        }

        /// <summary>
        /// Gets the response stream.
        /// </summary>
        /// <param name="requestUri">The request URI.</param>
        /// <param name="requestBody">The request body.</param>
        /// <param name="credentials">The credentials.</param>
        /// <param name="method">The method.</param>
        /// <param name="contentType">Type of the content.</param>
        /// <returns></returns>
        internal static Stream GetResponseStream(string requestUri, string requestBody, ICredentials credentials, string method, string contentType)
        {
            HttpWebRequest httpWebRequest = (HttpWebRequest)HttpWebRequest.Create(requestUri);
            httpWebRequest.UserAgent = "SubversionReports";
            httpWebRequest.Connection = "TE";
            httpWebRequest.Headers.Add("TE", "trailers");
            if (credentials != null)
                httpWebRequest.Credentials = credentials;
            httpWebRequest.Method = method;
            httpWebRequest.KeepAlive = false;
            httpWebRequest.ProtocolVersion = HttpVersion.Version10;
            byte[] bytes = Encoding.UTF8.GetBytes(requestBody);
            httpWebRequest.ContentLength = bytes.Length;
            Stream requestStream = httpWebRequest.GetRequestStream();
            requestStream.Write(bytes, 0, bytes.Length);
            requestStream.Close();
            if (!string.IsNullOrEmpty(contentType))
                httpWebRequest.ContentType = contentType;
            WebResponse webResponse = httpWebRequest.GetResponse();
            Stream responseStream = webResponse.GetResponseStream();
            //webResponse.Close();
            return responseStream;
        }

        /// <summary>
        /// Gets the response stream.
        /// </summary>
        /// <param name="uri">The URI.</param>
        /// <param name="credentials">The credentials.</param>
        /// <returns></returns>
        internal static Stream GetResponseStream(Uri uri, ICredentials credentials)
        {
            HttpWebRequest httpWebRequest = (HttpWebRequest)HttpWebRequest.Create(uri);
            httpWebRequest.UserAgent = "SubversionReports";
            if (credentials != null)
                httpWebRequest.Credentials = credentials;
            httpWebRequest.Method = "GET";
            httpWebRequest.KeepAlive = false;
            httpWebRequest.ProtocolVersion = HttpVersion.Version10;
            WebResponse webResponse = httpWebRequest.GetResponse();
            return webResponse.GetResponseStream();
        }

        /// <summary>
        /// Gets the repository title.
        /// </summary>
        /// <param name="repositoryUrl">The repository URL.</param>
        /// <param name="username">The username.</param>
        /// <param name="password">The password.</param>
        /// <returns></returns>
        private static string GetRepositoryTitle(string repositoryUrl, string username, string password)
        {
            var cleanUrl = GetGoogleCodeRepositoryRootUrl(repositoryUrl);
            var reader = new StreamReader(GetResponseStream(new Uri(cleanUrl), GetCredentialCache(cleanUrl, username, password)));

            var title = GetRepositoryTitle(reader);
            if (!string.IsNullOrEmpty(title))
                return title;

            var markup = reader.ReadToEnd();
            var start = markup.IndexOf("<title>") + 7;
            var end = markup.IndexOf("</title>") - start;
            return markup.Substring(start, end).Trim();
        }

        private static string GetRepositoryTitle(TextReader reader)
        {
            string title;
            try
            {
                var xdoc = XDocument.Load(reader).Document;
                title = xdoc != null && xdoc.Root != null
                    ? xdoc.Root.Descendants("index").Attributes("base").First().Value
                    : null;
            }
            catch
            {
                title = null;
            }
            return title;
        }

        /// <summary>
        /// Gets the google code repository root URL.
        /// </summary>
        /// <param name="repositoryUrl">The repository URL.</param>
        /// <returns></returns>
        private static string GetGoogleCodeRepositoryRootUrl(string repositoryUrl)
        {
            if (!repositoryUrl.ToLower().Contains(".googlecode.com/") || repositoryUrl.ToLower().EndsWith(".googlecode.com/svn/"))
                return repositoryUrl;
            if (repositoryUrl.ToLower().Contains("/svn/") && !repositoryUrl.ToLower().EndsWith("/svn/"))
                return repositoryUrl.Substring(0, repositoryUrl.IndexOf("/svn/") + 5);
            if (repositoryUrl.ToLower().EndsWith(".googlecode.com/svn"))
                return repositoryUrl + "/";
            throw new ArgumentException("Invalid Google Code subversion repository URL", "repositoryUrl");
        }

        #endregion

        #endregion

        #region revisions

        /// <summary>
        /// Gets the revision.
        /// </summary>
        /// <param name="repositoryUrl">The repository URL.</param>
        /// <param name="username">The username.</param>
        /// <param name="password">The password.</param>
        /// <param name="revisionId">The revision id.</param>
        /// <returns></returns>
        public static Revision GetRevision(string repositoryUrl, string username, string password, int revisionId)
        {
            return GetRepositoryLog(repositoryUrl, username, password).Find(delegate(Revision revision) { return revision.Id.Equals(revisionId); });
        }

        /// <summary>
        /// Gets the revision.
        /// </summary>
        /// <param name="repositoryUrl">The repository URL.</param>
        /// <param name="revisionId">The revision id.</param>
        /// <returns></returns>
        public static Revision GetRevision(string repositoryUrl, int revisionId)
        {
            return GetRevision(repositoryUrl, null, null, revisionId);
        }

        /// <summary>
        /// Gets the revision date.
        /// </summary>
        /// <param name="repositoryUrl">The repository URL.</param>
        /// <param name="username">The username.</param>
        /// <param name="password">The password.</param>
        /// <param name="revisionId">The revision id.</param>
        /// <returns></returns>
        public static DateTime GetRevisionDate(string repositoryUrl, string username, string password, int revisionId)
        {
            return GetRevision(repositoryUrl, username, password, revisionId).Date;
        }

        #endregion

        #region paths

        /// <summary>
        /// Gets the file log.
        /// </summary>
        /// <param name="repositoryUrl">The repository URL.</param>
        /// <param name="username">The username.</param>
        /// <param name="password">The password.</param>
        /// <param name="path">The path.</param>
        /// <returns></returns>
        public static Repository GetFileLog(string repositoryUrl, string username, string password, string path)
        {
            return GetRepositoryLog(repositoryUrl, username, password, 1, GetLastRevisionNumber(repositoryUrl, username, password)).Filter("Path", path, FilterType.Include);
        }

        /// <summary>
        /// Gets the last path revision id.
        /// </summary>
        /// <param name="repositoryUrl">The repository URL.</param>
        /// <param name="username">The username.</param>
        /// <param name="password">The password.</param>
        /// <param name="path">The path.</param>
        /// <returns></returns>
        public static int GetFirstPathRevisionId(string repositoryUrl, string username, string password, string path)
        {
            return GetFileLog(repositoryUrl, username, password, path).LowestRevision;
        }

        /// <summary>
        /// Gets the last path revision id.
        /// </summary>
        /// <param name="repositoryUrl">The repository URL.</param>
        /// <param name="username">The username.</param>
        /// <param name="password">The password.</param>
        /// <param name="path">The path.</param>
        /// <returns></returns>
        public static int GetLastPathRevisionId(string repositoryUrl, string username, string password, string path)
        {
            return GetFileLog(repositoryUrl, username, password, path).HighestRevision;
        }

        /// <summary>
        /// Gets the path children.
        /// </summary>
        /// <param name="repositoryUrl">The repository URL.</param>
        /// <param name="username">The username.</param>
        /// <param name="password">The password.</param>
        /// <param name="path">The path.</param>
        /// <param name="includeDeletedPaths">if set to <c>true</c> [include deleted paths].</param>
        /// <returns></returns>
        public static List<PathSummary> GetPathChildren(string repositoryUrl, string username, string password, string path, bool includeDeletedPaths)
        {
            int lastRepositoryRevision = 0; //only needed if moved entries are found
            PathSummaries paths = new PathSummaries();
            Regex pathsRegex = new Regex("<li><a[^>]*>([^<]*)</a></li>");
            foreach (Match match in pathsRegex.Matches(GetWebPage(string.Format("{0}{1}", repositoryUrl, path), username, password)))
            {
                if (match.Groups != null)
                {
                    string childPath = string.Format("{0}/{1}", path.Equals("/") ? string.Empty : path, match.Groups[1].Value.TrimEnd('/'));
                    bool isFolder = match.Groups[1].Value.EndsWith("/");
                    if (!childPath.EndsWith(".."))
                    {
                        Revision firstRevision = GetRevision(repositoryUrl, username, password, GetFirstPathRevisionId(repositoryUrl, username, password, childPath));
                        Revision lastRevision = GetRevision(repositoryUrl, username, password, GetLastPathRevisionId(repositoryUrl, username, password, childPath));
                        PathRevisionType lastRevisionType = (lastRevision == null || lastRevision.Paths == null) ? PathRevisionType.Unknown : lastRevision.Paths.Find(delegate(Path p1) { return p1.RevisionPath.Equals(childPath); }).RevisionType;
                        if (lastRevision == null)
                        {
                            if (lastRepositoryRevision.Equals(0))
                                lastRepositoryRevision = GetLastRevisionNumber(repositoryUrl, username, password);
                            string oldPath;
                            lastRevision = GetRevisionBeforePathChange(repositoryUrl, username, password, childPath, lastRepositoryRevision, out oldPath) ?? new Revision();
                            if (!string.IsNullOrEmpty(oldPath))
                            {
                                lastRevisionType = PathRevisionType.Moved;
                                string firstRevisionPath;
                                firstRevision = GetFirstRevisionOfChangedPath(repositoryUrl, username, password, oldPath, lastRevision.Id, out firstRevisionPath);
                            }
                        }
                        if (includeDeletedPaths || !lastRevisionType.Equals(PathRevisionType.Deleted))
                            paths.Add(
                                childPath,
                                isFolder,
                                firstRevision.Id,
                                lastRevision.Id,
                                firstRevision.Date,
                                lastRevision.Date,
                                firstRevision.Author,
                                lastRevision.Author,
                                lastRevisionType);
                    }
                }
            }

            List<PathSummary> folders = new List<PathSummary>();
            List<PathSummary> files = new List<PathSummary>();
            paths.ForEach(delegate(PathSummary ps) { if (ps.IsFolder) folders.Add(ps); else files.Add(ps); });
            folders.Sort(delegate(PathSummary p1, PathSummary p2) { return p1.FileName.CompareTo(p2.FileName); });
            files.Sort(delegate(PathSummary p1, PathSummary p2) { return p1.FileName.CompareTo(p2.FileName); });
            paths.Clear();
            paths.AddRange(folders);
            paths.AddRange(files);
            return paths;

        }

        /// <summary>
        /// Gets the revision before last path change.
        /// </summary>
        /// <param name="repositoryUrl">The repository URL.</param>
        /// <param name="username">The username.</param>
        /// <param name="password">The password.</param>
        /// <param name="currentPath">The current path.</param>
        /// <param name="currentRevision">The current revision.</param>
        /// <param name="lastChangedRevisionPath">The last changed revision path.</param>
        /// <returns></returns>
        private static Revision GetRevisionBeforePathChange(string repositoryUrl, string username, string password, string currentPath, int currentRevision, out string lastChangedRevisionPath)
        {
            int lastChangedRevisionId;
            GetLocationRevisionFromWebDav(repositoryUrl, username, password, currentRevision, currentPath, out lastChangedRevisionId, out lastChangedRevisionPath, false);
            if (lastChangedRevisionId > 0 && !string.IsNullOrEmpty(lastChangedRevisionPath))
                return GetRevision(repositoryUrl, username, password, lastChangedRevisionId);
            return null;
        }

        private static Revision GetFirstRevisionOfChangedPath(string repositoryUrl, string username, string password, string currentPath, int currentRevision, out string firstRevisionPath)
        {
            int firstRevisionId;
            GetLocationRevisionFromWebDav(repositoryUrl, username, password, currentRevision, currentPath, out firstRevisionId, out firstRevisionPath, true);
            if (firstRevisionId > 0 && !string.IsNullOrEmpty(firstRevisionPath) && !firstRevisionPath.Equals(currentPath))
                return GetRevision(repositoryUrl, username, password, firstRevisionId);
            return null;
        }

        /// <summary>
        /// Gets the repository paths.
        /// </summary>
        /// <param name="repositoryUrl">The repository URL.</param>
        /// <param name="username">The username.</param>
        /// <param name="password">The password.</param>
        /// <param name="includeDeletedPaths">if set to <c>true</c> [include deleted paths].</param>
        /// <returns></returns>
        public static List<string> GetRepositoryPathsFromLog(string repositoryUrl, string username, string password, bool includeDeletedPaths)
        {
            Repository repository = GetRepositoryLog(repositoryUrl, username, password);
            string cacheKey = string.Format("{0}-1-{1}-Paths", GetRepositoryHash(repositoryUrl), repository.HighestRevision);
            List<string> paths = CacheHelper<List<string>>.Get(cacheKey);
            if(paths == null)
            {
                paths = new List<string>();
                repository.ForEach(delegate(Revision revision) { revision.Paths.ForEach(delegate(Path path) { AddPathToPaths(paths, path.RevisionPath); }); });
                paths.Sort(delegate(string p1, string p2) { return p1.CompareTo(p2); });
                CacheHelper<List<string>>.Add(cacheKey, paths);
            }
            return paths;
        }

        /// <summary>
        /// Gets the repository path children.
        /// </summary>
        /// <param name="repositoryUrl">The repository URL.</param>
        /// <param name="username">The username.</param>
        /// <param name="password">The password.</param>
        /// <param name="path">The path.</param>
        /// <param name="includeDeletedPaths">if set to <c>true</c> [include deleted paths].</param>
        /// <returns></returns>
        public static List<string> GetRepositoryPathChildrenFromLog(string repositoryUrl, string username, string password, string path, bool includeDeletedPaths)
        {
            List<string> paths = new List<string>();
            GetRepositoryPathsFromLog(repositoryUrl, username, password, includeDeletedPaths).ForEach(delegate(string candidatePath) { if (IsDirectDescendant(path, candidatePath)) paths.Add(candidatePath); });
            return paths;
        }

        /// <summary>
        /// Adds the path (and parent paths) to paths.
        /// </summary>
        /// <param name="paths">The paths.</param>
        /// <param name="path">The path.</param>
        private static void AddPathToPaths(List<string> paths, string path)
        {
            if(!paths.Contains(path))
            {
                paths.Add(path);
                if (path.LastIndexOf('/') > 1)
                    AddPathToPaths(paths, path.Substring(0, path.LastIndexOf('/')));
            }
        }

        /// <summary>
        /// Determines whether the specified path is a folder (or a file).
        /// </summary>
        /// <param name="path">The path.</param>
        /// <returns>
        /// 	<c>true</c> if the specified path is folder; otherwise, <c>false</c>.
        /// </returns>
        public static bool IsFolder(string path)
        {
            //this needs refactoring to allow for files that don't have an extension
            //and folders that contain a dot.
            string last = path.Substring(path.LastIndexOf('/'));
            if (last.StartsWith("."))
                last = last.Substring(1);
            return (!last.Contains("."));
        }

        /// <summary>
        /// Determines whether [is direct descendant] [the specified parent].
        /// </summary>
        /// <param name="parent">The parent.</param>
        /// <param name="child">The child.</param>
        /// <returns>
        /// 	<c>true</c> if [is direct descendant] [the specified parent]; otherwise, <c>false</c>.
        /// </returns>
        private static bool IsDirectDescendant(string parent, string child)
        {
            if (parent.Equals("/") && !child.Equals("/") && !child.Substring(1).Contains("/"))
                return true;
            if (!child.Equals(parent) && child.StartsWith(parent + "/"))
                return child.Split('/').Length.Equals(parent.Split('/').Length + 1);
            return false;
        }

        #endregion

        #region authors


        /// <summary>
        /// Gets the author icon URL.
        /// </summary>
        /// <param name="repositoryUrl">The repository URL.</param>
        /// <param name="username">The username.</param>
        /// <param name="password">The password.</param>
        /// <param name="author">The author.</param>
        /// <returns></returns>
        public static string GetAuthorIconUrl(string repositoryUrl, string username, string password, string author)
        {
            return GetRepositoryLog(repositoryUrl, username, password).AuthorIcons[author];
        }

        /// <summary>
        /// Gets the author icon large URL.
        /// </summary>
        /// <param name="repositoryUrl">The repository URL.</param>
        /// <param name="username">The username.</param>
        /// <param name="password">The password.</param>
        /// <param name="author">The author.</param>
        /// <returns></returns>
        public static string GetAuthorIconLargeUrl(string repositoryUrl, string username, string password, string author)
        {
            return GetAuthorIconUrl(repositoryUrl, username, password, author).Replace("/Avatars/avatar", "/Avatars/Big/avatar");
        }

        /// <summary>
        /// Gets the author icon URL.
        /// </summary>
        /// <param name="repositoryUrl">The repository URL.</param>
        /// <param name="author">The author.</param>
        /// <returns></returns>
        public static string GetAuthorIconUrl(string repositoryUrl, string author)
        {
            return GetAuthorIconUrl(repositoryUrl, null, null, author);
        }

        #endregion

        #region filesystem cache

        /// <summary>
        /// Gets the file path to the filesystem log cache.
        /// </summary>
        /// <param name="repositoryUrl">The repository URL.</param>
        /// <returns></returns>
        private static string GetLogFilePath(string repositoryUrl)
        {
            return string.Format("{0}Logs{1}{2}.log", HttpContext.Current.Request.PhysicalApplicationPath, (HttpContext.Current.Request.PhysicalApplicationPath.StartsWith("/") ? "/" : "\\"), GetRepositoryHash(repositoryUrl));
        }

        /// <summary>
        /// Gets the repository hash.
        /// </summary>
        /// <param name="repositoryUrl">The repository URL.</param>
        /// <returns></returns>
        private static string GetRepositoryHash(string repositoryUrl)
        {
            return FormsAuthentication.HashPasswordForStoringInConfigFile(repositoryUrl, "md5");
        }

        #endregion
    }
}
